/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * @author Vladimir N. Molotkov, Stepan M. Mishura
 */

package org.apache.harmony.security.asn1;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

import org.apache.harmony.security.internal.nls.Messages;

/**
 * Decodes ASN.1 types encoded with BER (X.690)
 * 
 * @see http://asn1.elibel.tm.fr/en/standards/index.htm
 */

public class BerInputStream {

	/**
	 * Associated <code>InputStream</code>
	 */
	protected InputStream in;

	/**
	 * Internal buffer for storing encoded array
	 */
	protected byte[] buffer;

	/**
	 * The position in the buffer.
	 * 
	 * Next read must place data into the buffer from this offset
	 */
	protected int offset = 0;

	// The buffer increment size.
	// Must be reasonable big to reallocate memory not to often.
	// Primary is used for decoding indefinite length encoding
	private static final int BUF_INCREASE_SIZE = 1024 * 16;

	/**
	 * Indicates indefinite length of the current type
	 */
	protected static final int INDEFINIT_LENGTH = -1;

	/**
	 * Returns the length of the encoding
	 */
	public static int getLength(byte[] encoding) {
		int length = encoding[1] & 0xFF;
		int numOctets = 0;
		if ((length & 0x80) != 0) { // long form
			numOctets = length & 0x7F;

			// collect this value length
			length = encoding[2] & 0xFF;
			for (int i = 3; i < numOctets + 2; i++) {
				length = (length << 8) + (encoding[i] & 0xFF);
			}
		}
		// tag length long_form content
		return 1 + 1 + numOctets + length;
	}

	/**
	 * Current decoded tag
	 */
	public int tag;

	/**
	 * Current decoded length
	 */
	protected int length;

	/**
	 * Current decoded content
	 */
	public Object content;

	/**
	 * Current decoded tag offset
	 */
	protected int tagOffset;

	/**
	 * Current decoded content offset
	 */
	protected int contentOffset;

	/**
	 * The last choice index
	 */
	public int choiceIndex;

	/**
	 * Keeps last decoded: year, month, day, hour, minute, second, millisecond
	 */
	public int[] times;

	// FIXME comment me
	public int oidElement;

	/**
	 * Indicates verify or store mode.
	 * 
	 * In store mode a decoded content is stored in a newly allocated
	 * appropriate object. The <code>content</code> variable holds a reference
	 * to the last created object.
	 * 
	 * In verify mode a decoded content is not stored.
	 */
	// FIXME it is used only for one case
	// decoding PCKS#8 Private Key Info notation
	// remove this option because it does decoding more complex
	protected boolean isVerify;

	/**
	 * Indicates defined or indefined reading mode for associated InputStream.
	 * 
	 * This mode is defined by reading a length for a first ASN.1 type from
	 * InputStream.
	 */
	protected boolean isIndefinedLength;

	private Object[][] pool;

	/**
	 * Creates stream for decoding.
	 * 
	 * @param encoded
	 *            - bytes array to be decoded
	 * @throws IOException
	 *             - if an error occurs
	 */
	public BerInputStream(byte[] encoded) throws IOException {
		this(encoded, 0, encoded.length);
	}

	/**
	 * Creates stream for decoding.
	 * 
	 * @param encoded
	 *            - bytes array to be decoded
	 * @param offset
	 *            - the encoding offset
	 * @param expectedLength
	 *            - expected length of full encoding, this includes identifier,
	 *            length an content octets
	 * @throws IOException
	 *             - if an error occurs
	 */
	public BerInputStream(byte[] encoded, int offset, int expectedLength)
			throws IOException {

		buffer = encoded;
		this.offset = offset;

		next();

		// compare expected and decoded length
		if (length != INDEFINIT_LENGTH
				&& (offset + expectedLength) != (this.offset + length)) {
			throw new ASN1Exception(Messages.getString("security.111")); //$NON-NLS-1$
		}
	}

	/**
	 * Creates stream for decoding.
	 * 
	 * Allocates initial buffer of default size
	 * 
	 * @param is
	 *            associated <code>InputStream</code>
	 */
	public BerInputStream(InputStream in) throws IOException {
		this(in, BUF_INCREASE_SIZE);
	}

	/**
	 * Creates stream for decoding.
	 * 
	 * Allocates initial buffer of <code>initialSize</code> size
	 * 
	 * @param initialSize
	 *            the internal buffer initial size
	 * @param is
	 *            associated <code>InputStream</code>
	 */
	public BerInputStream(InputStream in, int initialSize) throws IOException {

		this.in = in;
		buffer = new byte[initialSize];

		next();

		if (length != INDEFINIT_LENGTH) {
			// input stream has definite length encoding
			// check allocated length to avoid further reallocations
			if (buffer.length < (length + offset)) {
				final byte[] newBuffer = new byte[length + offset];
				System.arraycopy(buffer, 0, newBuffer, 0, offset);
				buffer = newBuffer;
			}
		} else {
			isIndefinedLength = true;
			throw new ASN1Exception(Messages.getString("security.112")); //$NON-NLS-1$
		}
	}

	/**
	 * Reallocates the buffer in order to make it exactly the size of data it
	 * contains
	 */
	public void compactBuffer() {
		if (offset != buffer.length) {
			final byte[] newBuffer = new byte[offset];
			// restore buffer content
			System.arraycopy(buffer, 0, newBuffer, 0, offset);
			// set new buffer
			buffer = newBuffer;
		}
	}

	private final void decodeValueCollection(ASN1ValueCollection collection)
			throws IOException {

		final int begOffset = offset;
		final int endOffset = begOffset + length;

		final ASN1Type type = collection.type;

		if (isVerify) {
			while (endOffset > offset) {
				next();
				type.decode(this);
			}
		} else {

			final int seqTagOffset = tagOffset; // store tag offset

			final ArrayList<Object> values = new ArrayList<Object>();
			while (endOffset > offset) {
				next();
				values.add(type.decode(this));
			}

			content = values;

			tagOffset = seqTagOffset; // retrieve tag offset
		}

		if (offset != endOffset) {
			throw new ASN1Exception(Messages.getString(
					"security.134", begOffset)); //$NON-NLS-1$
		}
	}

	public Object get(Object key) {

		if (pool == null) {
			return null;
		}

		for (int i = 0; i < pool[0].length; i++) {
			if (pool[0][i] == key) {
				return pool[1][i];
			}
		}
		return null;
	}

	/**
	 * Returns internal buffer used for decoding
	 * 
	 * @return - buffer
	 */
	public final byte[] getBuffer() {
		return buffer;
	}

	public final int getContentOffset() {
		return contentOffset;
	}

	/**
	 * Returns encoded array.
	 * 
	 * MUST be invoked after decoding corresponding ASN.1 notation
	 */
	public byte[] getEncoded() {
		final byte[] encoded = new byte[offset - tagOffset];
		System.arraycopy(buffer, tagOffset, encoded, 0, encoded.length);
		return encoded;
	}

	/**
	 * Returns end offset for the current encoded type
	 * 
	 * @return - offset
	 */
	public final int getEndOffset() {
		return offset + length;
	}

	/**
	 * Returns length of the current content for decoding
	 * 
	 * @return - length of content
	 */
	public final int getLength() {
		return length;
	}

	/**
	 * Returns the current offset
	 * 
	 * @return - offset
	 */
	public final int getOffset() {
		return offset;
	}

	/**
	 * Returns start offset for the current encoded type
	 * 
	 * @return - offset
	 */
	public final int getTagOffset() {
		return tagOffset;
	}

	/**
	 * Decodes next encoded type. Initializes tag, length, tagOffset and
	 * contentOffset variables
	 * 
	 * @return next decoded tag
	 * @throws IOException
	 *             - if error occured
	 */
	public int next() throws IOException {

		tagOffset = offset;

		// read tag
		tag = read();

		// read length
		length = read();
		if (length != 0x80) { // definite form
			// long or short length form
			if ((length & 0x80) != 0) { // long form
				final int numOctets = length & 0x7F;

				if (numOctets > 5) {
					throw new ASN1Exception(Messages.getString("security.113", //$NON-NLS-1$
							tagOffset)); // FIXME message
				}

				// collect this value length
				length = read();
				for (int i = 1; i < numOctets; i++) {
					final int ch = read();
					length = (length << 8) + ch;// read();
				}

				if (length > 0xFFFFFF) {
					throw new ASN1Exception(Messages.getString("security.113", //$NON-NLS-1$
							tagOffset)); // FIXME message
				}
			}
		} else { // indefinite form
			length = INDEFINIT_LENGTH;
		}
		contentOffset = offset;

		return tag;
	}

	public void put(Object key, Object entry) {

		if (pool == null) {
			pool = new Object[2][10];
		}

		int i = 0;
		for (; i < pool[0].length && pool[0][i] != null; i++) {
			if (pool[0][i] == key) {
				pool[1][i] = entry;
				return;
			}
		}

		if (i == pool[0].length) {
			final Object[][] newPool = new Object[pool[0].length * 2][2];
			System.arraycopy(pool[0], 0, newPool[0], 0, pool[0].length);
			System.arraycopy(pool[1], 0, newPool[1], 0, pool[0].length);
			pool = newPool;
		} else {
			pool[0][i] = key;
			pool[1][i] = entry;
		}
	}

	/**
	 * Reads the next encoded byte from the encoded input stream.
	 * 
	 * @return the next encoded byte
	 * @throws IOException
	 *             - if error occured
	 */
	protected int read() throws IOException {

		if (offset == buffer.length) {
			throw new ASN1Exception(Messages.getString("security.13B")); //$NON-NLS-1$
		}

		if (in == null) {
			return buffer[offset++] & 0xFF;
		} else {
			final int octet = in.read();
			if (octet == -1) {
				throw new ASN1Exception(Messages.getString("security.13B")); //$NON-NLS-1$
			}

			buffer[offset++] = (byte) octet;

			return octet;
		}
	}

	/**
	 * Decodes ASN.1 bitstring type
	 * 
	 * @throws IOException
	 *             - if error occured
	 */
	public void readBitString() throws IOException {

		if (tag == ASN1Constants.TAG_BITSTRING) {

			if (length == 0) {
				throw new ASN1Exception(Messages.getString(
						"security.114", tagOffset)); //$NON-NLS-1$
			}

			readContent();

			// content: check unused bits
			if (buffer[contentOffset] > 7) {
				throw new ASN1Exception(Messages.getString("security.115", //$NON-NLS-1$
						contentOffset));
			}

			if (length == 1 && buffer[contentOffset] != 0) {
				throw new ASN1Exception(Messages.getString("security.116", //$NON-NLS-1$
						contentOffset));
			}

		} else if (tag == ASN1Constants.TAG_C_BITSTRING) {
			throw new ASN1Exception(Messages.getString("security.117")); //$NON-NLS-1$
		} else {
			throw new ASN1Exception(Messages.getString(
					"security.118", tagOffset, //$NON-NLS-1$
					Integer.toHexString(tag)));
		}
	}

	/**
	 * Decodes ASN.1 boolean type
	 * 
	 * @throws IOException
	 *             - if error occured
	 */
	public void readBoolean() throws IOException {

		if (tag != ASN1Constants.TAG_BOOLEAN) {
			throw new ASN1Exception(Messages.getString("security.11C", //$NON-NLS-1$
					tagOffset, Integer.toHexString(tag)));
		}

		// check encoded length
		if (length != 1) {
			throw new ASN1Exception(Messages.getString(
					"security.11D", tagOffset));//$NON-NLS-1$
		}

		readContent();
	}

	/**
	 * Reads the next encoded content from the encoded input stream. The method
	 * MUST be used for reading a primitive encoded content.
	 * 
	 * @throws IOException
	 *             - if error occured
	 */
	public void readContent() throws IOException {
		if (offset + length > buffer.length) {
			throw new ASN1Exception(Messages.getString("security.13B")); //$NON-NLS-1$
		}

		if (in == null) {
			offset += length;
		} else {
			int bytesRead = in.read(buffer, offset, length);

			if (bytesRead != length) {
				// if input stream didn't return all data at once
				// try to read it in several blocks
				int c = bytesRead;
				do {
					if (c < 1 || bytesRead > length) {
						throw new ASN1Exception(
								Messages.getString("security.13C")); //$NON-NLS-1$
					}
					c = in.read(buffer, offset + bytesRead, length - bytesRead);
					bytesRead += c;
				} while (bytesRead != length);
			}

			offset += length;
		}
	}

	/**
	 * Decodes ASN.1 Enumerated type
	 * 
	 * @throws IOException
	 *             - if error occured
	 */
	public void readEnumerated() throws IOException {

		if (tag != ASN1Constants.TAG_ENUM) {
			throw new ASN1Exception(Messages.getString(
					"security.119", tagOffset, //$NON-NLS-1$
					Integer.toHexString(tag)));
		}

		//
		// all checks are the same as for ASN.1 integer type
		//

		// check encoded length
		if (length == 0) {
			throw new ASN1Exception(Messages.getString(
					"security.11A", tagOffset));//$NON-NLS-1$
		}

		readContent();

		// check encoded content
		if (length > 1) {

			int bits = buffer[contentOffset] & 0xFF;
			if (buffer[contentOffset + 1] < 0) {
				bits += 0x100;
			}

			if (bits == 0 || bits == 0x1FF) {
				throw new ASN1Exception(Messages.getString(
						"security.11B", contentOffset)); //$NON-NLS-1$
			}
		}
	}

	/**
	 * Decodes ASN.1 GeneralizedTime type
	 * 
	 * @throws IOException
	 *             - if error occured
	 */
	public void readGeneralizedTime() throws IOException {

		if (tag == ASN1Constants.TAG_GENERALIZEDTIME) {

			// FIXME: any other optimizations?
			readContent();
			// FIXME store string somewhere to allow a custom time type perform
			// additional checks

			// check syntax: the last char MUST be Z
			if (buffer[offset - 1] != 'Z') {
				// FIXME support only format that is acceptable for DER
				throw new ASN1Exception(Messages.getString("security.11E")); //$NON-NLS-1$
			}

			// check syntax: MUST be YYYYMMDDHHMMSS[(./,)DDD]'Z'
			if (length != 15 && (length < 17 || length > 19)) // invalid
																// length
			{
				throw new ASN1Exception(Messages.getString("security.11F", //$NON-NLS-1$
						contentOffset));
			}

			// check content: milliseconds
			if (length > 16) {
				final byte char14 = buffer[contentOffset + 14];
				if (char14 != '.' && char14 != ',') {
					throw new ASN1Exception(Messages.getString("security.11F", //$NON-NLS-1$
							contentOffset));
				}
			}

			if (times == null) {
				times = new int[7];
			}
			times[0] = strToInt(contentOffset, 4); // year
			times[1] = strToInt(contentOffset + 4, 2); // month
			times[2] = strToInt(contentOffset + 6, 2); // day
			times[3] = strToInt(contentOffset + 8, 2); // hour
			times[4] = strToInt(contentOffset + 10, 2); // minute
			times[5] = strToInt(contentOffset + 12, 2); // second

			if (length > 16) {
				// FIXME optimize me
				times[6] = strToInt(contentOffset + 15, length - 16);

				if (length == 17) {
					times[6] = times[6] * 100;
				} else if (length == 18) {
					times[6] = times[6] * 10;
				}
			}

			// FIXME check all values for valid numbers!!!
		} else if (tag == ASN1Constants.TAG_C_GENERALIZEDTIME) {
			throw new ASN1Exception(Messages.getString("security.120")); //$NON-NLS-1$

		} else {
			throw new ASN1Exception(Messages.getString("security.121", //$NON-NLS-1$
					tagOffset, Integer.toHexString(tag)));
		}
	}

	/**
	 * Decodes ASN.1 Integer type
	 * 
	 * @throws IOException
	 *             - if error occured
	 */
	public void readInteger() throws IOException {

		if (tag != ASN1Constants.TAG_INTEGER) {
			throw new ASN1Exception(Messages.getString("security.127", //$NON-NLS-1$
					tagOffset, Integer.toHexString(tag)));
		}

		// check encoded length
		if (length < 1) {
			throw new ASN1Exception(Messages.getString("security.128", //$NON-NLS-1$
					tagOffset));
		}

		readContent();

		// check encoded content
		if (length > 1) {

			final byte firstByte = buffer[offset - length];
			final byte secondByte = (byte) (buffer[offset - length + 1] & 0x80);

			if (firstByte == 0 && secondByte == 0 || firstByte == (byte) 0xFF
					&& secondByte == (byte) 0x80) {
				throw new ASN1Exception(Messages.getString("security.129", //$NON-NLS-1$
						(offset - length)));
			}
		}
	}

	/**
	 * Decodes ASN.1 Octetstring type
	 * 
	 * @throws IOException
	 *             - if error occured
	 */
	public void readOctetString() throws IOException {

		if (tag == ASN1Constants.TAG_OCTETSTRING) {
			readContent();
		} else if (tag == ASN1Constants.TAG_C_OCTETSTRING) {
			throw new ASN1Exception(Messages.getString("security.12A")); //$NON-NLS-1$
		} else {
			throw new ASN1Exception(Messages.getString(
					"security.12B", tagOffset, //$NON-NLS-1$
					Integer.toHexString(tag)));
		}
	}

	/**
	 * Decodes ASN.1 ObjectIdentifier type
	 * 
	 * @throws IOException
	 *             - if error occured
	 */
	public void readOID() throws IOException {

		if (tag != ASN1Constants.TAG_OID) {
			throw new ASN1Exception(Messages.getString("security.12C", //$NON-NLS-1$
					tagOffset, Integer.toHexString(tag)));
		}

		// check encoded length
		if (length < 1) {
			throw new ASN1Exception(Messages.getString(
					"security.12D", tagOffset)); //$NON-NLS-1$
		}

		readContent();

		// check content: last encoded byte (8th bit MUST be zero)
		if ((buffer[offset - 1] & 0x80) != 0) {
			throw new ASN1Exception(Messages.getString(
					"security.12E", (offset - 1))); //$NON-NLS-1$
		}

		oidElement = 1;
		for (int i = 0; i < length; i++, ++oidElement) {

			// According to ASN.1 BER spec:
			// leading octet of subidentifier MUST not be 0x80
			// This assertion is not verified
			//
			// if (buffer[contentOffset + i] == (byte)0x80) {
			// throw new ASN1Exception(
			// "Wrong content for ASN.1 object identifier at ["
			// + contentOffset
			// +
			// "]. Subidentifier MUST be encoded in minimum number of octets");
			// }

			while ((buffer[contentOffset + i] & 0x80) == 0x80) {
				i++;
			}
		}
	}

	/**
	 * Decodes ASN.1 Sequence type
	 * 
	 * @param sequence
	 *            - ASN.1 sequence to be decoded
	 * @throws IOException
	 *             - if error occured
	 */
	public void readSequence(ASN1Sequence sequence) throws IOException {

		if (tag != ASN1Constants.TAG_C_SEQUENCE) {
			throw new ASN1Exception(Messages.getString(
					"security.12F", tagOffset, //$NON-NLS-1$
					Integer.toHexString(tag)));
		}

		final int begOffset = offset;
		final int endOffset = begOffset + length;

		final ASN1Type[] type = sequence.type;

		int i = 0;

		if (isVerify) {

			for (; (offset < endOffset) && (i < type.length); i++) {

				next();
				while (!type[i].checkTag(tag)) {
					// check whether it is optional component or not
					if (!sequence.OPTIONAL[i] || (i == type.length - 1)) {
						throw new ASN1Exception(Messages.getString(
								"security.130", //$NON-NLS-1$
								tagOffset));
					}
					i++;
				}

				type[i].decode(this);
			}

			// check the rest of components
			for (; i < type.length; i++) {
				if (!sequence.OPTIONAL[i]) {
					throw new ASN1Exception(Messages.getString("security.131", //$NON-NLS-1$
							tagOffset));
				}
			}

		} else {

			final int seqTagOffset = tagOffset; // store tag offset

			final Object[] values = new Object[type.length];
			for (; (offset < endOffset) && (i < type.length); i++) {

				next();
				while (!type[i].checkTag(tag)) {
					// check whether it is optional component or not
					if (!sequence.OPTIONAL[i] || (i == type.length - 1)) {
						throw new ASN1Exception(Messages.getString(
								"security.132", //$NON-NLS-1$
								tagOffset));
					}

					// sets default value
					if (sequence.DEFAULT[i] != null) {
						values[i] = sequence.DEFAULT[i];
					}
					i++;
				}
				values[i] = type[i].decode(this);
			}

			// check the rest of components
			for (; i < type.length; i++) {
				if (!sequence.OPTIONAL[i]) {
					throw new ASN1Exception(Messages.getString("security.133", //$NON-NLS-1$
							tagOffset));
				}
				if (sequence.DEFAULT[i] != null) {
					values[i] = sequence.DEFAULT[i];
				}
			}
			content = values;

			tagOffset = seqTagOffset; // retrieve tag offset
		}

		if (offset != endOffset) {
			throw new ASN1Exception(Messages.getString(
					"security.134", begOffset)); //$NON-NLS-1$
		}
	}

	/**
	 * Decodes ASN.1 SequenceOf type
	 * 
	 * @param sequenceOf
	 *            - ASN.1 sequence to be decoded
	 * @throws IOException
	 *             - if error occured
	 */
	public void readSequenceOf(ASN1SequenceOf sequenceOf) throws IOException {

		if (tag != ASN1Constants.TAG_C_SEQUENCEOF) {
			throw new ASN1Exception(Messages.getString(
					"security.135", tagOffset, //$NON-NLS-1$
					Integer.toHexString(tag)));
		}

		decodeValueCollection(sequenceOf);
	}

	/**
	 * Decodes ASN.1 Set type
	 * 
	 * @param set
	 *            - ASN.1 set to be decoded
	 * @throws IOException
	 *             - if error occured
	 */
	public void readSet(ASN1Set set) throws IOException {

		if (tag != ASN1Constants.TAG_C_SET) {
			throw new ASN1Exception(Messages.getString("security.136", //$NON-NLS-1$
					tagOffset, Integer.toHexString(tag)));
		}

		throw new ASN1Exception(Messages.getString("security.137")); //$NON-NLS-1$
	}

	/**
	 * Decodes ASN.1 SetOf type
	 * 
	 * @param set
	 *            - ASN.1 set to be decoded
	 * @throws IOException
	 *             - if error occured
	 */
	public void readSetOf(ASN1SetOf setOf) throws IOException {

		if (tag != ASN1Constants.TAG_C_SETOF) {
			throw new ASN1Exception(Messages.getString("security.138", //$NON-NLS-1$
					tagOffset, Integer.toHexString(tag)));
		}

		decodeValueCollection(setOf);
	}

	/**
	 * Decodes ASN.1 String type
	 * 
	 * @throws IOException
	 *             - if an I/O error occurs or the end of the stream is reached
	 */
	public void readString(ASN1StringType type) throws IOException {

		// FIXME check string content
		if (tag == type.id) {
			readContent();
		} else if (tag == type.constrId) {
			throw new ASN1Exception(Messages.getString("security.139")); //$NON-NLS-1$
		} else {
			throw new ASN1Exception(Messages.getString(
					"security.13A", tagOffset, //$NON-NLS-1$
					Integer.toHexString(tag)));
		}
	}

	// // reallocates internal buffer for indefined reading mode
	// private void reallocateBuffer(int n) {
	// int newSize;
	// for (newSize = buffer.length * 2; newSize < buffer.length + n; newSize =
	// newSize * 2)
	// ;
	// byte[] newBuffer = new byte[newSize];
	// System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
	// buffer = newBuffer;
	// }

	/**
	 * Decodes ASN.1 UTCTime type
	 * 
	 * @throws IOException
	 *             - if an I/O error occurs or the end of the stream is reached
	 */
	public void readUTCTime() throws IOException {

		if (tag == ASN1Constants.TAG_UTCTIME) {

			switch (length) {
			case ASN1UTCTime.UTC_HM:
			case ASN1UTCTime.UTC_HMS:
				break;
			case ASN1UTCTime.UTC_LOCAL_HM:
			case ASN1UTCTime.UTC_LOCAL_HMS:
				// FIXME only coordinated universal time formats are supported
				throw new ASN1Exception(Messages.getString("security.122")); //$NON-NLS-1$
			default:
				throw new ASN1Exception(Messages.getString("security.123", //$NON-NLS-1$
						tagOffset));
			}

			// FIXME: any other optimizations?
			readContent();

			// FIXME store string somewhere to allow a custom time type perform
			// additional checks

			// check syntax: the last char MUST be Z
			if (buffer[offset - 1] != 'Z') {
				throw new ASN1Exception("ASN.1 UTCTime wrongly encoded at [" //$NON-NLS-1$
						+ contentOffset + ']');
			}

			if (times == null) {
				times = new int[7];
			}

			times[0] = strToInt(contentOffset, 2); // year
			if (times[0] > 49) {
				times[0] += 1900;
			} else {
				times[0] += 2000;
			}

			times[1] = strToInt(contentOffset + 2, 2); // month
			times[2] = strToInt(contentOffset + 4, 2); // day
			times[3] = strToInt(contentOffset + 6, 2); // hour
			times[4] = strToInt(contentOffset + 8, 2); // minute

			if (length == ASN1UTCTime.UTC_HMS) {
				times[5] = strToInt(contentOffset + 10, 2); // second
			}

			// FIXME check all time values for valid numbers!!!
		} else if (tag == ASN1Constants.TAG_C_UTCTIME) {
			throw new ASN1Exception(Messages.getString("security.124")); //$NON-NLS-1$
		} else {
			throw new ASN1Exception(Messages.getString("security.125", //$NON-NLS-1$
					tagOffset, Integer.toHexString(tag)));
		}
	}

	//
	//
	//
	//
	//

	/**
	 * Resets this stream to initial state.
	 * 
	 * @param encoded
	 *            - a new bytes array to be decoded
	 * @throws IOException
	 *             - if an error occurs
	 */
	public final void reset(byte[] encoded) throws IOException {
		buffer = encoded;

		next();
	}

	/**
	 * Sets verify mode.
	 */
	public final void setVerify() {
		isVerify = true;
	}

	// TODO comment me
	private int strToInt(int off, int count) throws ASN1Exception {

		// FIXME works only with buffer

		int c;
		int result = 0;
		for (int i = off, end = off + count; i < end; i++) {
			c = buffer[i] - 48;
			if (c < 0 || c > 9) {
				throw new ASN1Exception(Messages.getString("security.126")); //$NON-NLS-1$
			}
			result = result * 10 + c;
		}
		return result;
	}
}
